/*
			       ___ _      _   ___ _  _    ___  _  _ 
			      | __| |    /_\ / __| || |  / _ \| \| |
			      | _|| |__ / _ \\__ \ __ | | (_) | .` |
			      |_| |____/_/ \_\___/_||_|  \___/|_|\_|
			                                            
			  _____ _  _   _ _____   ___ ___ _____ ___ _  _ 
			 |_   _| || | /_\_   _| | _ )_ _|_   _/ __| || |
			   | | | __ |/ _ \| |   | _ \| |  | || (__| __ |
			   |_| |_||_/_/ \_\_|   |___/___| |_| \___|_||_|
			        F L A S H  O N  T H A T  B I T C H
			        
		+--------------------------------------------------------+
		|	This file is delivered as is. Any modifications to   |
		|   this class and/or sub classes may cause errors and   |
		|   can cause you compilations to fail. FOTB takes no	 |
		|   responsibility for any complications caused by the	 |
		|   following code or any modified versions of it.		 |
		+--------------------------------------------------------+

*/

package com.flashonthatbitch.filesystem {

/*
		 ___ __  __ ___  ___  ___ _____ 
		|_ _|  \/  | _ \/ _ \| _ \_   _|
		 | || |\/| |  _/ (_) |   / | |  
		|___|_|  |_|_|  \___/|_|_\ |_|  
		I M P O R T
*/
	
	import com.flashonthatbitch.filesystem.events.FolderEvent;
	
	import flash.desktop.Icon;
	import flash.errors.IOError;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.TimerEvent;
	import flash.filesystem.File;
	import flash.system.Capabilities;
	import flash.utils.Timer;
	
	/** */
	[Event(name = "folderSelected", type = "com.flashonthatbitch.filesystem.events.FolderEvent")]
	/** */
	[Event(name = "folderCancel", type = "com.flashonthatbitch.filesystem.events.FolderEvent")]
	/** */
	[Event(name = "folderComplete", type = "com.flashonthatbitch.filesystem.events.FolderEvent")]
	/** */
	[Event(name = "folderChange", type = "com.flashonthatbitch.filesystem.events.FolderEvent")]
	
	public class Folder extends EventDispatcher implements IFile {
		
		// sorting options
		/**	*/
		public static const SORTING_NAME	:String 	= "name";
		/**	*/
		public static const SORTING_TYPE	:String 	= "type"
		/**	*/
		public static const SORTING_CREATED	:String 	= "created";
		
		// statics
		private static const sysExtensions	:Array 		= ["sys","cmd","com","bat","dll","ocx","DS_Store","localized","tmp","bck","ini","inf","db","plist"];
		private static var   os			:String		= null;
		
		// controls
		private var $file					:FileObject;
		private var $icon					:IconObject;
		
		// monitor controls
		private var $monitor				:FolderMonitor;
		
		// values
		private var $nativePath				:String;
		private var $removeFlag				:String;
		
		// lists
		private var $files					:Array;
		private var $folders				:Array;
		private var $packages				:Array;
		private var $ignored				:Array;
		
		// settings
		private var $ignoreSystemFileObjects		:Boolean 	= true;
		private var $ignoreHiddenFileObjects		:Boolean 	= true;
		private var $ignoreSymbolicLinks			:Boolean	= true;
		
/*
		  ___ ___  _  _ ___ _____ ___ _   _  ___ _____ ___  ___ 
		 / __/ _ \| \| / __|_   _| _ \ | | |/ __|_   _/ _ \| _ \
		| (_| (_) | .` \__ \ | | |   / |_| | (__  | || (_) |   /
		 \___\___/|_|\_|___/ |_| |_|_\\___/ \___| |_| \___/|_|_\
		C O N S T R U C T O R
*/

		/**
		 * Creates a Folder object.
		 * 
		 * @param nativePath				Path to folder
		 * @param ignoreHiddeFileObjects		Ignore hidden files
		 * @param ignoreSystemFileObjects	Ignore system files
		 * @param ignoreSymbolicLinks		Ignore symbolic links (such as "alias" for osx and .lnk for win)
		 * 
		 */
		public function Folder(nativePath:String = null, ignoreHiddenFileObjects:Boolean = true, ignoreSystemFileObjects:Boolean = true, ignoreSymbolicLinks:Boolean =  true) {
			
			$nativePath 					= nativePath;
			$ignoreHiddenFileObjects 		= ignoreHiddenFileObjects;
			$ignoreSystemFileObjects 		= ignoreSystemFileObjects;
			$ignoreSymbolicLinks			= ignoreSymbolicLinks;
			
			construct();
			
			if(nativePath != null)
				parsePath(nativePath);
			
		}

/*		
		 ___ _   _ ___ _    ___ ___ 
		| _ \ | | | _ ) |  |_ _/ __|
		|  _/ |_| | _ \ |__ | | (__ 
		|_|  \___/|___/____|___\___|
		P U B L I C
*/
		
		/**
		 * Retrieves all items in the current folder
		 * sorted in the specified order.
		 * 
		 * Available orders are:
		 * .SORTING_NAME
		 * .SORTING_TYPE
		 * .SORTING_CREATED
		 *  
		 * @param order		Sorting order
		 * @return 			A sorted array
		 * 
		 */
		public function getItemsSortedBy(order:String="type"):Array {
			var ai:Array = allItems;
			switch(order){
				case SORTING_NAME :
					ai.sortOn("name"); break;
				case SORTING_CREATED :
					ai.sortOn("creationDate"); break;
				case SORTING_TYPE :
				default:
					ai.sortOn("extension");
			}
			return ai;
		}
		
		/**
		 * Opens a native system browse dialog for folder browsing.
		 * Executes a FolderEvent.SELECTED or FolderEvent.CANCEL
		 * upon user-interaction.
		 * 
		 * @see FolderEvent.SELECTED
		 * @see FolderEvent.CANCEL 
		 * 
		 */
		public function browse():void {
			$file.addEventListener(Event.SELECT, browse_folderSelected);
			$file.addEventListener(Event.CANCEL, browse_folderCancel);
			$file.browseForDirectory("Browse for folder...");
		}

		/**
		 * Monitors changes in the folder and dispatches a
		 * FolderEvent.CHANGE if any change is noticed.
		 * 
		 * @see	FolderEvent.CHANGE 
		 * @param frequecy	Frequency of the change lookup
		 * 
		 */
		public function monitor(frequecy:uint=10000):void {
			$monitor = new FolderMonitor($file,frequecy);
			$monitor.addEventListener(FolderEvent.CHANGE, function(e:FolderEvent){ dispatchEvent(e); } );
		}
		
		/**
		 * Clones the folder. An equaly specified Folder object 
		 * will be created and returned. No copy of the actual folder
		 * will be performed.
		 *  
		 * @return		 A Folder clone
		 * 
		 */
		public function clone():Folder {
			return new Folder(nativePath,$ignoreHiddenFileObjects,$ignoreSystemFileObjects);
		}
		
		/**
		 * Removes the folder in two steps for added security.
		 * First call to remove() will return a verification string.
		 * This string is then reqired at the second call which will
		 * remove the folder. If the verification string is invalid
		 * or empty, the remove() will be reset.
		 * 
		 * @param	verification	Verification code
		 * @return	A verification code (one time use only)
		 */
		public function remove(verification:String = null):String {
			if (!verification) {
				$removeFlag = "FS" + Math.floor((Math.random() * 0xFFFFFFFF));
				return $removeFlag;
			}else if (verification == $removeFlag) {
				$file.deleteDirectory();
			}
			$removeFlag = null;
			return null;
		}
		
		/**
		 * Cleans the directory from all content.
		 * Warning: This will delete everything inside the folder!
		 *  
		 * @return		True if successful 
		 * 
		 */
		public function clean():Boolean {
			if(exists){
				try {
					$file.deleteDirectory(true);
				}catch(e:*){
					return false;
				}
			}else
				return false;
			if(isTrackedByMonitor)
				$monitor.kill();
			return true;
		}
		
		/**
		 * Checks if a specific file exists in the folder.
		 *  
		 * @param name		FileObject name
		 * @param extension	FileObject extension
		 * @return 			true if the file exists, otherwise false
		 * 
		 */
		public function hasFileObject(name:String,extension:String="*"):Boolean {
			var file:FileObject = getFileObject(name,extension);
			if(file!=null)
				return true;
			return false;
		}
		
		/**
		 * Retrieves a FileObject by specified name (and extension).
		 * If more than one file is found, the first match will
		 * be returned. 
		 * 
		 * @param name		FileObject name
		 * @param extension	FileObject extenstion
		 * @return 			A FileObject object
		 * 
		 */
		public function getFileObject(name:String,extension:String="*"):FileObject {
			if(extension != "*" && extensions.indexOf(extension) == -1)
				return null;
			var list:Array; 
			if(extension == "*")	list = allItems;
			else					list = getFileObjectsByExtension(extension);
			for each(var file:FileObject in list)
				if(file.name == name)	return file;
			return null;
		}
		
		/**
		 * Retrieves all files with the specified extension.
		 * 
		 * @param extension	FileObject extension
		 * @return 			Array with FileObject objects
		 * 
		 */
		public function getFileObjectsByExtension(extension:String="*"):Array {
			if(extensions.indexOf(extension) == -1)
				return [];
			var ret:Array;
			if(extension == "*")	ret = allItems;
			else{
				ret = new Array();
				for each(var file:FileObject in allItems){
					if(file.extension == extension)		ret.push(file);
				}
			}
			return ret;
		}
		
		/**
		 * Searches for FileObjects that matches the
		 * needle by their name or extension.
		 *  
		 * @param needle	Search string
		 * @return 			Array with matching FileObjects
		 * 
		 */
		public function search(needle:String,byExtension:Boolean=false):Array {
			var ret:Array = [];
			if(byExtension && extensions.indexOf(needle) > -1 || !byExtension)
			for each(var f:FileObject in $files)
				if(byExtension && f.extension.indexOf(needle) > -1)
						ret.push(f);
				else if(f.name.indexOf(needle) > -1)
					ret.push(f);
			return ret;
		}
		
		/**
		 * Searches for Folders that matches the
		 * needle by their name.
		 *  
		 * @param needle	Search string
		 * @return 			Array with matching FileObjects
		 * 
		 */
		public function searchFolders(needle:String):Array {
			var ret:Array = [];
			for each(var f:Folder in $folders)
				if(f.name.indexOf(needle) > -1)
					ret.push(f);
			return ret;
		}

/*
		 ___ ___ _____   ___ _____ ___ 
		| _ \ _ \_ _\ \ / /_\_   _| __|
		|  _/   /| | \ V / _ \| | | _| 
		|_| |_|_\___| \_/_/ \_\_| |___|
		P R I V A T E
*/
		
		/**
		 * [internal-use]
		 * Constructs the basics. 
		 * 
		 */
		private function construct():void {
			
			$file 		= new FileObject();
			$folders 	= new Array();
			$files 		= new Array();
			$packages	= new Array();
			$ignored	= new Array();
		}
		
		/**
		 * [internal-use]
		 * Parses the path for files, folders and packages.
		 * The result is pushed into the lists for future use.
		 * Dispatches a FolderEvent.COMPLETE when finished.
		 * 
		 * @throws 				FolderError
		 * @param nativePath	Path to folder
		 * 
		 */		
		private function parsePath(nativePath:String=null):void {
			
			clear();
			if(nativePath) {
				$file.nativePath = nativePath;
			}else if ($nativePath){
				$file.nativePath = $nativePath;
			}else {
				throw getError("No 'nativePath' is defined.");
			}
			if($file.isDirectory){
				for each(var file:File in $file.getDirectoryListing()){
					if(file.isDirectory){
						if(!$ignoreHiddenFileObjects || $ignoreHiddenFileObjects && !file.isHidden || !$ignoreSystemFileObjects || $ignoreSystemFileObjects && file.name.slice(0,1) != "." )
							$folders.push(file);
					}else if(file.isPackage)
						$packages.push(file);
					else{
						if(file.name.substr(0,1) == "." ){
							//trace(". name");
							$ignored.push(file);
						}else if($ignoreHiddenFileObjects && file.isHidden){
							//trace("hidden");
							$ignored.push(file);
						}else if($ignoreSystemFileObjects && sysExtensions.indexOf( file.extension ) > -1){
							//trace("system");
							$ignored.push(file);
						}else if ($ignoreSymbolicLinks && file.isSymbolicLink){
							//trace("symbolic link");
							$ignored.push(file);
						}else{
							$files.push(file);
						}
					}
				}
				dispatchEvent(new FolderEvent(FolderEvent.COMPLETE));
			}else{
				throw getError("The specified nativePath is not a folder.");
			}
			
		}
		
		/**
		 * [internal-use]
		 * Executed when a Folder.browse selectes a folder.
		 *  
		 * @param e		Event
		 * 
		 */
		private function browse_folderSelected(e:Event):void {
			$file.removeEventListener(Event.CANCEL, browse_folderCancel);
			$file.removeEventListener(Event.SELECT, browse_folderSelected);
			$nativePath = e.target.nativePath;
			parsePath($nativePath);
			dispatchEvent(new FolderEvent(FolderEvent.SELECTED));
		}
		
		/**
		 * [internal-use]
		 * Executed when a Folder.browse cancel is made by the user.
		 *  
		 * @param e		Event
		 * 
		 */
		private function browse_folderCancel(e:Event):void {
			$file.removeEventListener(Event.CANCEL, browse_folderCancel);
			$file.removeEventListener(Event.SELECT, browse_folderSelected);
			dispatchEvent(new FolderEvent(FolderEvent.CANCEL));
		}
		
		/**
		 * [internal-use]
		 * Clears all lists. 
		 * 
		 */
		private function clear():void {
			if ($files.length > 0) {
				$files.splice(0, $files.length);
			}
			if ($folders.length > 0) {
				$folders.splice(0, $folders.length);
			}
			if ($packages.length > 0) {
				$packages.splice(0, $packages.length);
			}
			if ($ignored.length > 0) {
				$ignored.splice(0, $ignored.length);
			}
		}

/*
		  ___ ___ _____   ___ ___ _____ 
		 / __| __|_   _| / __| __|_   _|
		| (_ | _|  | |   \__ \ _|  | |  
		 \___|___| |_|   |___/___| |_|  
		G E T  S E T
*/

		/**
		 * Traces the current state of the folder.
		 * 
		 */	
		public function expose():void {
			trace("--- " + name + " ---")
			if(isEmpty){
				trace("Folder is empty\n---");
				return;
			}				
			if($folders.length > 0){
				trace("folders:");
				for each(var file:FileObject in $folders)
					trace("--> " + file.name);
			}
			if($packages.length > 0){
				trace("packages:");
				for each(file in $packages)
					trace("--* " + file.name);
			}
			if($files.length > 0){
				trace("files:");
				for each(file in $files)
					trace("--+ " + file.name);
			}
			trace("---");
		}
		
		/**
		 * Retrieves the folder name.
		 *  
		 * @return		Folder name 
		 * 
		 */
		public function get name():String {
			return $file.name;
		}
		
		/**
		 * Retrieves all listed files in the folder.
		 * Note: if any ignores are set, these files will
		 * not be listed here either.
		 *  
		 * @return 		A list of files
		 * 
		 */
		public function get files():Array {
			return $files;			
		}
		
		/**
		 * Returns an array with Folder objects.
		 *  
		 * @return 
		 * 
		 */
		public function get folders():Array {
			var ret:Array = [];
			for each(var folder:File in foldersAsFiles)
				ret.push(new Folder(folder.nativePath,$ignoreHiddenFileObjects,$ignoreSystemFileObjects));
			return ret;
		}
		
		/**
		 * Retrieves a list of folders in this folder.
		 * Note: these folders will NOT be Folder objects,
		 * but FileObject objects.
		 *  
		 * @return 		List of folders
		 * 
		 */
		public function get foldersAsFiles():Array {
			return $folders;
		}
		
		/**
		 * The native path (system specific) to this folder.
		 * 
		 * @see FileObject.nativePath
		 * @return 		Path to folder (System specific format)
		 * 
		 */
		public function get nativePath():String {
			return $file.nativePath;
		}
		
		/**
		 * Retrieves a list of packages in this folder.
		 * Note: the packages in this list will be represented
		 * as FileObject objects.
		 *  
		 * @return		List of packages 
		 * 
		 */
		public function get packages():Array {
			return $packages;
		}
		
		/**
		 * Returns true if the folder is empty.
		 * 
		 * @return		True if empty 
		 * 
		 */
		public function get isEmpty():Boolean {
			if($files.length+$packages.length+$folders.length <= 0)
				return true;
			return false;
		}
		
		/**
		 * Returns true if the specified folder is a volume.
		 *  
		 * @return 		True if the folder is a volume
		 * 
		 */
		public function get isVolume():Boolean {
			if(!os){
				if(Capabilities.os.toLowerCase().indexOf("mac") != -1)
					os = "osx";
				else if(Capabilities.os.toLowerCase().indexOf("win") != -1)
					os = "windows";
			}
			switch(os){
				case "windows" :
					return ($file.nativePath.indexOf(":\\") > 0 && $file.nativePath.length < 4);
				case "osx" :
				default:
					return ($file.nativePath.indexOf("/Volumes/") >= 0 && $file.nativePath.lastIndexOf("/") == 8);
			}
		}
		
		/**
		 * Checks if the current folder is the root directory.
		 * Note: this only works on unix based systems. 
		 *  
		 * @return 		True if the folder is the root
		 * 
		 */
		public function get isRoot():Boolean {
			return ($file.nativePath.length == 1 && $file.nativePath.indexOf("/") == 0);
		}
		
		/**
		 * Checks if the folder is tracked by a monitor.
		 *  
		 * @return		True if it is 
		 * 
		 */
		public function get isTrackedByMonitor():Boolean {
			return (($monitor)?true:false);
		}
		
		/**
		 * Retrieves a list of all file extensions in
		 * this folder.
		 *  
		 * @return		A list of extensions 
		 * 
		 */
		public function get extensions():Array {
			var ret:Array = new Array();
			for each(var item:FileObject in $files)
				if(ret.indexOf(item.extension.toLowerCase()) == -1)
					ret.push(item.extension.toLowerCase());
			return ret; 
		}
		
		/**
		 * Retrieves all items in all lists.
		 * Note: if ignorefilters are on; affected files will
		 * not be listed here. 
		 * 
		 * @return 		A list of all folder, files and packages combined
		 * 
		 */
		public function get allItems():Array {
			var ret:Array = new Array();
			ret.push.apply(this,$folders);
			ret.push.apply(this,$packages);
			ret.push.apply(this,$files);
			return ret;
		}
		
		/**
		 * Retrieves all files that are ignored.
		 * Default files are: system files, symboliclinks and hidden files.
		 */
		public function get ignoredFiles():Array {
			return $ignored;
		}
		
		/**
		 * Retrieves an Icon object.
		 *  
		 * @return		Icon object 
		 * 
		 */
		public function get icons():IconObject {
			if ($icon == null)
				$icon = new IconObject($file.icon);
			return $icon;
		}
		
		/**
		 * Calculates and returns the total size of the folder.
		 * Note: This does not include sub-folders.
		 *  
		 * @return		Size in bytes 
		 * 
		 */
		public function get size():Number {
			var totalSize:Number = 0;
			for each(var f:File in $file.getDirectoryListing()){
				try{
					totalSize+=f.size;
				}catch(e:IOError){
					//ignore;
				}
			}
			return totalSize;
		}
		
		/**
		 * Returns the available space.
		 *  
		 * @return		Number of bytes available 
		 * 
		 */
		public function get spaceAvailable():Number {
			return $file.spaceAvailable;
		}
		
		/**
		 * Parent folder.
		 *  
		 * @return		The parent folder as a Folder object 
		 * 
		 */
		public function get parent():Folder {
			return new Folder($file.parent.nativePath);
		}
		
		/**
		 * Creation date of the folder.
		 *  
		 * @return		Creation date as Date object 
		 * 
		 */
		public function get creationDate():Date {
			return $file.creationDate;
		}
		
		/**
		 * Returns true if the folder exists, otherwise false.
		 * 
		 * @return		Existence 
		 * 
		 */
		public function get exists():Boolean {
			return $file.exists;
		}
		
		/**
		 * Returns a string representation of this Folder instance.
		 *  
		 * @return		A summary string 
		 * 
		 */
		override public function toString():String {
			if($nativePath != null)
				return '[Folder name="' + name + '" numFileObjects="' + $files.length + '" numFolders="' + $folders.length + '" numPackages="' + $packages.length + '" size="' + size + '" isVolume="' + isVolume + '" isRoot="' + isRoot + '" nativePath="' + nativePath + '"]';
			return '[object Folder]';
		}
		
		/**
		 * [internal-use]
		 * Returns a pre-defined error with the specified message.
		 *  
		 * @param message		Message
		 * @return 				FolderError object
		 * 
		 */
		private function getError(message:String):Error {
			var err:Error = new Error(message);
			err.name = "FolderError";
			return err;
		}
		
	}
	
}

/*
		 ___ __  __ ___  ___  ___ _____ 
		|_ _|  \/  | _ \/ _ \| _ \_   _|
		 | || |\/| |  _/ (_) |   / | |  
		|___|_|  |_|_|  \___/|_|_\ |_|  
		I M P O R T
*/

import flash.utils.Timer;
import flash.events.TimerEvent;
import com.flashonthatbitch.filesystem.FileObject;
import flash.events.EventDispatcher;
import com.flashonthatbitch.filesystem.events.FolderEvent;

class FolderMonitor extends EventDispatcher {
	
	private static const TOLERANCE	:uint = 3;
	
	private var $monitor		:Timer;	
	private var $folder			:FileObject;
	private var $modDate		:Number	= 0;
	private var $toleranceTick	:uint = 0;

/*
		  ___ ___  _  _ ___ _____ ___ _   _  ___ _____ ___  ___ 
		 / __/ _ \| \| / __|_   _| _ \ | | |/ __|_   _/ _ \| _ \
		| (_| (_) | .` \__ \ | | |   / |_| | (__  | || (_) |   /
		 \___\___/|_|\_|___/ |_| |_|_\\___/ \___| |_| \___/|_|_\
		C O N S T R U C T O R
*/
	
	/**
	 * Creates a new FolderMonitor
	 *  
	 * @param folder		Folder to track
	 * @param frequency		Frequence of lookups in ms
	 * 
	 */
	public function FolderMonitor(folder:FileObject,frequency:uint = 10000){
		$folder = folder;
		$modDate = $folder.modificationDate.getTime();
		$monitor = new Timer(frequency,0);
		$monitor.addEventListener(TimerEvent.TIMER, monitor_changeHandler);
		$monitor.start();
	}
	
	/**
	 * Kills the monitoring cycle. 
	 * 
	 */
	public function kill():void {
		$monitor.stop();
	}

/*
		 ___ ___ _____   ___ _____ ___ 
		| _ \ _ \_ _\ \ / /_\_   _| __|
		|  _/   /| | \ V / _ \| | | _| 
		|_| |_|_\___| \_/_/ \_\_| |___|
		P R I V A T E
*/
	
	/**
	 * [internal-use]
	 * Checks if the folder has changed.
	 *  
	 * @param e		TimerEvent
	 * 
	 */
	private function monitor_changeHandler(e:TimerEvent):void {
		try {
			if($modDate != $folder.modificationDate.getTime()){
				$modDate = $folder.modificationDate.getTime();
				dispatchEvent(new FolderEvent(FolderEvent.CHANGE));
			}
			$toleranceTick=0;
		}catch(e:*){
			$toleranceTick++;
			if($toleranceTick == TOLERANCE)
				kill();
		}
	}
	
}